#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
PDF批量转图片工具
支持批量转换PDF文件为图片，可自定义输出格式和质量
"""

import os
import sys
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from pathlib import Path
from typing import List, Optional
import threading
import io
import fitz  # PyMuPDF
from PIL import Image, ImageEnhance

# 尝试导入拖拽功能，如果失败则禁用拖拽
try:
    import tkinterdnd2 as tkdnd
    DRAG_DROP_AVAILABLE = True
except ImportError:
    DRAG_DROP_AVAILABLE = False
    print("警告: 未安装tkinterdnd2，拖拽功能将被禁用")


class PDFToImageConverter:
    """PDF转图片转换器"""
    
    def __init__(self):
        self.pdf_files = []
        self.output_dir = ""
        self.image_format = "PNG"
        self.dpi = 300
        self.quality = 95
        self.merge_pages = False  # 是否合并所有页面为一张长图
        
    def select_pdf_files(self, file_paths: List[str]) -> bool:
        """选择PDF文件并验证"""
        valid_files = []
        
        for file_path in file_paths:
            if not os.path.exists(file_path):
                print(f"文件不存在: {file_path}")
                continue
                
            if not file_path.lower().endswith('.pdf'):
                print(f"非PDF文件: {file_path}")
                continue
                
            # 尝试打开PDF验证文件完整性
            try:
                doc = fitz.open(file_path)
                if doc.page_count == 0:
                    print(f"PDF文件为空: {file_path}")
                    doc.close()
                    continue
                doc.close()
                valid_files.append(file_path)
            except Exception as e:
                print(f"无法打开PDF文件 {file_path}: {str(e)}")
                continue
        
        if len(valid_files) > 30:
            print("警告: 选择的PDF文件超过30个，将只处理前30个文件")
            valid_files = valid_files[:30]
            
        self.pdf_files = valid_files
        return len(valid_files) > 0
    
    def set_output_directory(self, output_dir: str) -> bool:
        """设置输出目录"""
        try:
            Path(output_dir).mkdir(parents=True, exist_ok=True)
            self.output_dir = output_dir
            return True
        except Exception as e:
            print(f"无法创建输出目录 {output_dir}: {str(e)}")
            return False
    
    def convert_pdf_to_images(self, pdf_path: str, progress_callback=None) -> bool:
        """将单个PDF转换为图片"""
        try:
            doc = fitz.open(pdf_path)
            pdf_name = Path(pdf_path).stem
            
            total_pages = doc.page_count
            
            if self.merge_pages and total_pages > 1:
                # 合并所有页面为一张长图
                return self._convert_pdf_to_long_image(doc, pdf_name, progress_callback)
            else:
                # 分页转换
                return self._convert_pdf_to_separate_images(doc, pdf_name, progress_callback)
            
        except Exception as e:
            print(f"转换PDF文件失败 {pdf_path}: {str(e)}")
            return False
    
    def _convert_pdf_to_separate_images(self, doc, pdf_name: str, progress_callback=None) -> bool:
        """将PDF转换为单独的页面图片"""
        try:
            total_pages = doc.page_count
            converted_pages = 0
            
            for page_num in range(total_pages):
                page = doc.load_page(page_num)
                
                # 设置渲染参数
                mat = fitz.Matrix(self.dpi / 72, self.dpi / 72)
                pix = page.get_pixmap(matrix=mat)
                
                # 生成输出文件名
                if total_pages == 1:
                    output_filename = f"{pdf_name}.{self.image_format.lower()}"
                else:
                    output_filename = f"{pdf_name}_page_{page_num + 1:03d}.{self.image_format.lower()}"
                
                output_path = os.path.join(self.output_dir, output_filename)
                
                # 保存图片
                if self.image_format.upper() == "PNG":
                    pix.save(output_path)
                else:
                    # JPEG格式：先保存为PNG再转换
                    temp_png = output_path.replace('.jpeg', '.png').replace('.jpg', '.png')
                    pix.save(temp_png)
                    
                    # 用PIL转换为JPEG
                    with Image.open(temp_png) as img:
                        rgb_img = img.convert('RGB')  # 确保是RGB模式
                        rgb_img.save(output_path, "JPEG", quality=self.quality)
                    
                    # 删除临时PNG文件
                    os.remove(temp_png)
                
                converted_pages += 1
                
                # 更新进度
                if progress_callback:
                    progress = (converted_pages / total_pages) * 100
                    progress_callback(pdf_name, progress, converted_pages, total_pages)
            
            doc.close()
            return True
            
        except Exception as e:
            error_msg = f"分页转换失败: {str(e)}"
            print(error_msg)
            import traceback
            traceback.print_exc()
            doc.close()
            return False
    
    def _convert_pdf_to_long_image(self, doc, pdf_name: str, progress_callback=None) -> bool:
        """将PDF的所有页面合并为一张长图"""
        try:
            total_pages = doc.page_count
            page_images = []
            max_width = 0
            
            # 首先渲染所有页面并收集图片
            for page_num in range(total_pages):
                page = doc.load_page(page_num)
                
                # 设置渲染参数
                mat = fitz.Matrix(self.dpi / 72, self.dpi / 72)
                pix = page.get_pixmap(matrix=mat)
                
                # 转换为PIL Image
                img_data = pix.tobytes("ppm")
                pil_image = Image.open(io.BytesIO(img_data))
                page_images.append(pil_image)
                
                # 记录最大宽度
                if pil_image.width > max_width:
                    max_width = pil_image.width
                
                # 更新进度
                if progress_callback:
                    progress = ((page_num + 1) / total_pages) * 50  # 前50%进度用于渲染
                    progress_callback(f"{pdf_name} - 渲染页面", progress, page_num + 1, total_pages)
            
            # 计算总高度
            total_height = sum(img.height for img in page_images)
            
            # 创建长图画布
            long_image = Image.new('RGB', (max_width, total_height), 'white')
            
            # 将所有页面图片粘贴到长图上
            current_height = 0
            for i, img in enumerate(page_images):
                # 如果图片宽度小于最大宽度，居中放置
                x_offset = (max_width - img.width) // 2
                long_image.paste(img, (x_offset, current_height))
                current_height += img.height
                
                # 更新进度
                if progress_callback:
                    progress = 50 + ((i + 1) / len(page_images)) * 50  # 后50%进度用于合并
                    progress_callback(f"{pdf_name} - 合并页面", progress, i + 1, len(page_images))
            
            # 保存长图
            output_filename = f"{pdf_name}_merged.{self.image_format.lower()}"
            output_path = os.path.join(self.output_dir, output_filename)
            
            if self.image_format.upper() == "PNG":
                long_image.save(output_path, "PNG")
            else:
                long_image.save(output_path, "JPEG", quality=self.quality)
            
            doc.close()
            return True
            
        except Exception as e:
            error_msg = f"长图转换失败: {str(e)}"
            print(error_msg)
            import traceback
            traceback.print_exc()
            doc.close()
            return False
    
    def batch_convert(self, progress_callback=None) -> dict:
        """批量转换PDF文件"""
        if not self.pdf_files:
            return {"success": False, "message": "没有选择PDF文件"}
        
        if not self.output_dir:
            return {"success": False, "message": "没有设置输出目录"}
        
        results = {
            "success": True,
            "total_files": len(self.pdf_files),
            "converted_files": 0,
            "failed_files": [],
            "message": ""
        }
        
        for i, pdf_path in enumerate(self.pdf_files):
            pdf_name = Path(pdf_path).name
            
            if progress_callback:
                progress_callback(f"正在处理: {pdf_name}", (i / len(self.pdf_files)) * 100, i, len(self.pdf_files))
            
            if self.convert_pdf_to_images(pdf_path, progress_callback):
                results["converted_files"] += 1
            else:
                results["failed_files"].append(pdf_name)
        
        if results["failed_files"]:
            results["message"] = f"成功转换 {results['converted_files']} 个文件，失败 {len(results['failed_files'])} 个文件"
        else:
            results["message"] = f"成功转换所有 {results['converted_files']} 个PDF文件"
        
        return results


class PDFConverterGUI:
    """PDF转图片工具GUI界面"""
    
    def __init__(self):
        self.converter = PDFToImageConverter()
        # 根据是否支持拖拽选择不同的Tk
        if DRAG_DROP_AVAILABLE:
            self.root = tkdnd.Tk()
        else:
            self.root = tk.Tk()
        self.setup_gui()
        
    def setup_gui(self):
        """设置GUI界面"""
        self.root.title("PDF批量转图片工具")
        self.root.resizable(True, True)
        self.root.minsize(800, 730)
        
        # 创建可滚动的主框架
        self.canvas = tk.Canvas(self.root, highlightthickness=0)
        self.scrollbar = ttk.Scrollbar(self.root, orient="vertical", command=self.canvas.yview)
        self.scrollable_frame = ttk.Frame(self.canvas)
        
        # 配置滚动
        self.scrollable_frame.bind("<Configure>", self.on_frame_configure)
        self.canvas.bind("<Configure>", self.on_canvas_configure)
        
        self.canvas_window = self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
        self.canvas.configure(yscrollcommand=self.scrollbar.set)
        
        # 鼠标滚轮支持
        def _on_mousewheel(event):
            if self.scrollbar.winfo_viewable():
                self.canvas.yview_scroll(int(-1*(event.delta/120)), "units")
        self.root.bind_all("<MouseWheel>", _on_mousewheel)
        
        # 布局组件
        self.canvas.pack(side="left", fill="both", expand=True, padx=5, pady=5)
        
        # 主内容框架（在可滚动区域内）
        main_frame = ttk.Frame(self.scrollable_frame, padding="15")
        main_frame.pack(fill="both", expand=True)
        
        # 配置网格权重
        main_frame.columnconfigure(1, weight=1)
        
        # PDF文件选择区域
        pdf_header_frame = ttk.Frame(main_frame)
        pdf_header_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5)
        pdf_header_frame.columnconfigure(1, weight=1)
        
        ttk.Label(pdf_header_frame, text="1. 选择PDF文件 (最多30个):").grid(row=0, column=0, sticky=tk.W)
        self.file_count_var = tk.StringVar(value="已选择: 0 个文件")
        ttk.Label(pdf_header_frame, textvariable=self.file_count_var, foreground="blue").grid(row=0, column=1, sticky=tk.E)
        
        pdf_frame = ttk.Frame(main_frame)
        pdf_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5)
        pdf_frame.columnconfigure(0, weight=1)
        
        # 按钮框架
        button_frame = ttk.Frame(pdf_frame)
        button_frame.grid(row=0, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 5))
        
        ttk.Button(button_frame, text="选择PDF文件", command=self.select_pdf_files).grid(row=0, column=0, sticky=tk.W)
        ttk.Button(button_frame, text="删除选中", command=self.remove_selected_pdf).grid(row=0, column=1, sticky=tk.W, padx=(10, 0))
        
        # 只有在支持拖拽时才显示拖拽提示
        if DRAG_DROP_AVAILABLE:
            ttk.Label(button_frame, text="或拖拽PDF文件到下方列表", foreground="gray").grid(row=0, column=2, sticky=tk.W, padx=(10, 0))
        
        self.pdf_listbox = tk.Listbox(pdf_frame, height=8)
        self.pdf_listbox.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5)
        
        # 启用拖拽功能（如果可用）
        if DRAG_DROP_AVAILABLE:
            self.setup_drag_drop()
        
        # 添加右键菜单
        self.setup_context_menu()
        
        pdf_scrollbar = ttk.Scrollbar(pdf_frame, orient=tk.VERTICAL, command=self.pdf_listbox.yview)
        pdf_scrollbar.grid(row=1, column=2, sticky=(tk.N, tk.S))
        self.pdf_listbox.config(yscrollcommand=pdf_scrollbar.set)
        
        # 输出设置区域
        ttk.Label(main_frame, text="2. 输出设置:").grid(row=2, column=0, sticky=tk.W, pady=(15, 5))
        
        settings_frame = ttk.LabelFrame(main_frame, text="输出设置", padding="8")
        settings_frame.grid(row=3, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5)
        settings_frame.columnconfigure(1, weight=1)
        
        # 输出目录选择
        ttk.Label(settings_frame, text="输出目录:").grid(row=0, column=0, sticky=tk.W, pady=2)
        self.output_dir_var = tk.StringVar()
        ttk.Entry(settings_frame, textvariable=self.output_dir_var, state="readonly").grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(5, 0), pady=2)
        ttk.Button(settings_frame, text="浏览", command=self.select_output_dir).grid(row=0, column=2, padx=(5, 0), pady=2)
        
        # 图片格式选择
        ttk.Label(settings_frame, text="图片格式:").grid(row=1, column=0, sticky=tk.W, pady=2)
        self.format_var = tk.StringVar(value="PNG")
        format_combo = ttk.Combobox(settings_frame, textvariable=self.format_var, values=["PNG", "JPEG"], state="readonly")
        format_combo.grid(row=1, column=1, sticky=tk.W, padx=(5, 0), pady=2)
        
        # DPI设置
        ttk.Label(settings_frame, text="DPI (分辨率):").grid(row=2, column=0, sticky=tk.W, pady=2)
        self.dpi_var = tk.StringVar(value="300")
        dpi_combo = ttk.Combobox(settings_frame, textvariable=self.dpi_var, values=["150", "300", "450", "600"], state="readonly")
        dpi_combo.grid(row=2, column=1, sticky=tk.W, padx=(5, 0), pady=2)
        
        # JPEG质量设置
        ttk.Label(settings_frame, text="JPEG质量 (1-100):").grid(row=3, column=0, sticky=tk.W, pady=2)
        self.quality_var = tk.StringVar(value="95")
        quality_spin = ttk.Spinbox(settings_frame, from_=1, to=100, textvariable=self.quality_var)
        quality_spin.grid(row=3, column=1, sticky=tk.W, padx=(5, 0), pady=2)
        
        # 转换模式选择
        ttk.Label(settings_frame, text="转换模式:").grid(row=4, column=0, sticky=tk.W, pady=2)
        self.merge_mode_var = tk.StringVar(value="分页转换")
        mode_combo = ttk.Combobox(settings_frame, textvariable=self.merge_mode_var, 
                                values=["分页转换", "合并长图"], state="readonly")
        mode_combo.grid(row=4, column=1, sticky=tk.W, padx=(5, 0), pady=2)
        
        # 转换模式说明（换行显示）
        mode_help1 = ttk.Label(settings_frame, text="• 分页转换: 每页生成一张独立图片", foreground="gray", font=("Arial", 9))
        mode_help1.grid(row=5, column=0, columnspan=3, sticky=tk.W, pady=(2, 0))
        mode_help2 = ttk.Label(settings_frame, text="• 合并长图: 多页合并成一张竖向长图", foreground="gray", font=("Arial", 9))
        mode_help2.grid(row=6, column=0, columnspan=3, sticky=tk.W, pady=(0, 5))
        
        # 转换按钮区域
        ttk.Label(main_frame, text="3. 开始转换:").grid(row=4, column=0, sticky=tk.W, pady=(15, 5))
        
        button_frame = ttk.Frame(main_frame)
        button_frame.grid(row=5, column=0, columnspan=2, sticky=tk.W, pady=(5, 8))
        
        self.convert_button = ttk.Button(button_frame, text="开始转换", command=self.start_conversion)
        self.convert_button.pack(side=tk.LEFT, padx=(0, 10))
        
        ttk.Button(button_frame, text="清空列表", command=self.clear_pdf_list).pack(side=tk.LEFT)
        
        # 进度显示区域
        progress_frame = ttk.LabelFrame(main_frame, text="转换进度", padding="6")
        progress_frame.grid(row=6, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(8, 5))
        progress_frame.columnconfigure(0, weight=1)
        
        self.progress_var = tk.StringVar(value="准备就绪")
        progress_label = ttk.Label(progress_frame, textvariable=self.progress_var, font=("Arial", 9))
        progress_label.grid(row=0, column=0, sticky=tk.W, pady=(0, 3))
        
        self.progress_bar = ttk.Progressbar(progress_frame, mode='determinate')
        self.progress_bar.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(0, 3))
        
    def on_frame_configure(self, event=None):
        """当内容框架大小改变时"""
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))
        self.update_scrollbar_visibility()
    
    def on_canvas_configure(self, event=None):
        """当画布大小改变时"""
        # 调整内容框架宽度以匹配画布宽度
        canvas_width = event.width if event else self.canvas.winfo_width()
        self.canvas.itemconfig(self.canvas_window, width=canvas_width)
        self.update_scrollbar_visibility()
    
    def update_scrollbar_visibility(self):
        """根据内容是否超出窗口决定是否显示滚动条"""
        self.root.update_idletasks()  # 确保布局完成
        
        # 获取画布和内容的高度
        canvas_height = self.canvas.winfo_height()
        content_height = self.scrollable_frame.winfo_reqheight()
        
        if content_height > canvas_height and canvas_height > 1:
            # 需要滚动条
            if not self.scrollbar.winfo_viewable():
                self.scrollbar.pack(side="right", fill="y")
        else:
            # 不需要滚动条
            if self.scrollbar.winfo_viewable():
                self.scrollbar.pack_forget()
    
    def setup_drag_drop(self):
        """设置拖拽功能"""
        self.pdf_listbox.drop_target_register(tkdnd.DND_FILES)
        self.pdf_listbox.dnd_bind('<<Drop>>', self.on_drop)
    
    def setup_context_menu(self):
        """设置右键菜单"""
        self.context_menu = tk.Menu(self.root, tearoff=0)
        self.context_menu.add_command(label="删除选中文件", command=self.remove_selected_pdf)
        
        def show_context_menu(event):
            # 选中右键点击的项目
            clicked_index = self.pdf_listbox.nearest(event.y)
            if clicked_index < self.pdf_listbox.size():
                self.pdf_listbox.selection_clear(0, tk.END)
                self.pdf_listbox.selection_set(clicked_index)
                self.pdf_listbox.activate(clicked_index)
                
                # 显示菜单
                try:
                    self.context_menu.tk_popup(event.x_root, event.y_root)
                finally:
                    self.context_menu.grab_release()
        
        self.pdf_listbox.bind("<Button-3>", show_context_menu)  # Windows右键
        self.pdf_listbox.bind("<Button-2>", show_context_menu)  # macOS右键
    
    def on_drop(self, event):
        """处理拖拽文件事件"""
        files = self.root.tk.splitlist(event.data)
        pdf_files = [f for f in files if f.lower().endswith('.pdf')]
        
        if pdf_files:
            if self.converter.select_pdf_files(pdf_files):
                self.update_pdf_list()
            else:
                messagebox.showerror("错误", "拖拽的文件中没有有效的PDF文件")
        else:
            messagebox.showwarning("警告", "请拖拽PDF文件")
    
    def select_pdf_files(self):
        """选择PDF文件"""
        file_paths = filedialog.askopenfilenames(
            title="选择PDF文件",
            filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")]
        )
        
        if file_paths:
            if self.converter.select_pdf_files(list(file_paths)):
                self.update_pdf_list()
            else:
                messagebox.showerror("错误", "没有选择有效的PDF文件")
    
    def update_pdf_list(self):
        """更新PDF文件列表显示"""
        self.pdf_listbox.delete(0, tk.END)
        for pdf_path in self.converter.pdf_files:
            self.pdf_listbox.insert(tk.END, Path(pdf_path).name)
        
        # 更新文件数量显示
        file_count = len(self.converter.pdf_files)
        self.file_count_var.set(f"已选择: {file_count} 个文件")
    
    def select_output_dir(self):
        """选择输出目录"""
        output_dir = filedialog.askdirectory(title="选择输出目录")
        if output_dir:
            self.output_dir_var.set(output_dir)
            self.converter.set_output_directory(output_dir)
    
    def clear_pdf_list(self):
        """清空PDF列表"""
        self.converter.pdf_files.clear()
        self.update_pdf_list()
        # 重置进度条和状态
        self.progress_var.set("准备就绪")
        self.progress_bar['value'] = 0
    
    def remove_selected_pdf(self):
        """删除选中的PDF文件"""
        selection = self.pdf_listbox.curselection()
        if not selection:
            messagebox.showwarning("提示", "请先选择要删除的PDF文件")
            return
        
        selected_index = selection[0]
        selected_file = Path(self.converter.pdf_files[selected_index]).name
        
        # 确认删除
        if messagebox.askyesno("确认删除", f"确定要从列表中删除文件 '{selected_file}' 吗？"):
            # 从文件列表中删除
            del self.converter.pdf_files[selected_index]
            # 更新界面显示
            self.update_pdf_list()
    
    
    def update_progress(self, status: str, progress: float, current: int, total: int):
        """更新进度显示"""
        self.progress_var.set(f"{status} ({current}/{total})")
        self.progress_bar['value'] = progress
        self.root.update_idletasks()
    
    def start_conversion(self):
        """开始转换"""
        if not self.converter.pdf_files:
            messagebox.showerror("错误", "请先选择PDF文件")
            return
        
        if not self.output_dir_var.get():
            messagebox.showerror("错误", "请选择输出目录")
            return
        
        # 更新转换器设置
        self.converter.image_format = self.format_var.get()
        self.converter.dpi = int(self.dpi_var.get())
        self.converter.quality = int(self.quality_var.get())
        self.converter.merge_pages = (self.merge_mode_var.get() == "合并长图")
        
        # 禁用转换按钮
        self.convert_button.config(state="disabled")
        
        
        # 在新线程中执行转换
        thread = threading.Thread(target=self.conversion_thread)
        thread.daemon = True
        thread.start()
    
    def conversion_thread(self):
        """转换线程"""
        try:
            results = self.converter.batch_convert(self.update_progress)
            
            self.progress_var.set("转换完成")
            
            if results["success"] and not results["failed_files"]:
                messagebox.showinfo("成功", results["message"])
            elif results["failed_files"]:
                messagebox.showwarning("部分成功", results["message"])
            else:
                messagebox.showerror("失败", results["message"])
                
        except Exception as e:
            error_msg = f"转换过程中发生错误: {str(e)}"
            messagebox.showerror("错误", error_msg)
        finally:
            # 重新启用转换按钮
            self.convert_button.config(state="normal")
    
    def run(self):
        """运行GUI"""
        self.root.mainloop()


def main():
    """主函数"""
    try:
        app = PDFConverterGUI()
        app.run()
    except Exception as e:
        print(f"程序启动失败: {str(e)}")
        input("按回车键退出...")


if __name__ == "__main__":
    main()
